// 引入类型
import type {
  RouterOptions,
  Router,
  App,
  ReactiveRoute,
  RouteLocationNormalized,
  RouteRecordNormalized,
  guardFn,
  Lazy
} from './types'
import { createRouterMatcher } from './matcher'
import { computed, reactive, shallowRef, unref } from 'vue'
import { START_LOCATION_NORMALIZED } from './contant'
import { useCallback } from './utils'
import { RouterLink } from './router-link'
import { RouterView } from './router-view'

export function createRouter(options: RouterOptions): Router {
  // 获取 history 对象
  const routerHistory = options.history

  // 创建 matcher 对象
  const matcher = createRouterMatcher(options.routes)

  // 当前路由，一开始给了一个初始值
  const currentRoute = shallowRef(START_LOCATION_NORMALIZED)

  // 导航守卫
  const beforeGuards = useCallback()
  const beforeResolveGuards = useCallback()
  const afterGuards = useCallback()

  // 之后定义了一堆方法

  // 用来根据当前的路径来解析信息
  // 本质上是调用 matcher.resolve 方法
  function resolve(to: RouteLocationNormalized | string) {
    if (typeof to === 'string') {
      return matcher.resolve({
        path: to
      })
    } else {
      return matcher.resolve({
        path: to.path
      })
    }
  }

  // 记录哪些路由是要离开，哪些路由是要更新，哪些路由是要进入
  function extractChangeRecords(to: RouteLocationNormalized, from: RouteLocationNormalized) {
    // 离开
    const leavingRecords = []
    // 更新
    const updatingRecords = []
    // 进入
    const enteringRecords = []

    // 这里的 to 和 from 都有一个 matched，两者的长度是有所不同的
    // 因为我们既要遍历 to，也要遍历 from，为了两个都能过全部遍历到，我们取两者之间最长的那个
    const len = Math.max(to.matched.length, from.matched.length)
    for (let i = 0; i < len; i++) {
      // 拿到对应下表的 routeNormalized
      const recordFrom = from.matched[i]
      // 需要判断一下，因为有可能为 undefined
      if (recordFrom) {
        // 如果 to 的路由信息和 from 一样的，那么该路由对应的 vue 组件就是更新
        if (to.matched.find((record) => record.path === recordFrom.path)) {
          updatingRecords.push(recordFrom)
        } else {
          // 如果没有，那么就是离开，那么将当前的路由记录项推入 leavingRecords 里面
          leavingRecords.push(recordFrom)
        }
      }
      // 拿到当前的 to 所对应的路由项
      const recordTo = to.matched[i]
      if (recordTo) {
        // 查找 from 中是否有相同的路由记录，如果没有，那么 recordTo 就是一个新的要进入的路由记录
        if (!from.matched.find((record) => record.path === recordTo.path)) {
          enteringRecords.push(recordTo)
        }
      }
    }

    return [leavingRecords, updatingRecords, enteringRecords]
  }

  // 执行守卫回调，将执行结果转为 promise 返回
  function guardToPromise(
    guard: guardFn,
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    record?: RouteRecordNormalized
  ): () => Promise<void> {
    return () =>
      new Promise((resolve) => {
        // 这个 next 的作用是拿来解析 promise
        // 这个方法其实就是外部开发者所调用的 next 方法
        const next = () => resolve()
        // 调用导航守卫 guard 方法，并且将 to、from 和 next 作为参数传递过去
        // 所以用户在书写导航守卫回调函数的时候，会自动传入 to、from 以及 next
        const guardReturn = guard.call(record, to, from, next)
        // 将 guard 的返回值 guardReturn 包装为一个 promise，然后在这个 promise 解析后调用 next
        return Promise.resolve(guardReturn).then(next)
      })
  }

  // 从路由记录所对应的组件里面提取出指定类型的守卫回调
  function extractComponentsGuards(
    matched: RouteRecordNormalized[],
    guardType: string,
    to: RouteLocationNormalized,
    from: RouteLocationNormalized
  ) {
    // 存储所提取出来的守卫回调
    const guards = []

    for (const record of matched) {
      // 提取出来组件
      const rawComponent = record.components.default
      // 获取组件上面对应类型的守卫回调
      const guard = (rawComponent as any)[guardType]
      // 将对应的守卫回调推入到 guards
      // 这里需要注意，在推入进去的时候，需要将这个导航守卫转为 promise
      guard && guards.push(guardToPromise(guard, to, from, record))
    }

    return guards
  }

  // 按照顺序去执行一个路由守卫数组里面的所有函数
  function runGuardQueue(guards: Lazy<any>[]) {
    return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve())
  }

  // 导航方法
  // 这个方法主要是为了处理导航守卫
  async function navigate(to: RouteLocationNormalized, from: RouteLocationNormalized) {
    // 1. 既然要导航，那么就涉及到组件的更新
    // 在做导航的时候，我们需要知道哪些组件会进入，哪些组件会离开，还有哪些组件要更新
    const [leavingRecords, updatingRecords, enteringRecords] = extractChangeRecords(to, from)

    // 2. 导航的时候还会涉及到导航守卫的执行
    let guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from)

    // 3. 通过循环来处理要离开的路由所对应的组件级别的守卫回调
    for (const record of leavingRecords) {
      record.leaveGuards.forEach((guard) => {
        guards.push(guardToPromise(guard, to, from))
      })
    }

    // 4. 执行导航守卫队列
    return runGuardQueue(guards)
      .then(() => {
        // 不停的执行不同类型的守卫回调
        guards = []
        // 拿到 beforeEach 类型的守卫回调
        for (const guard of beforeGuards.list()) {
          // 推入到队列里面
          guards.push(guardToPromise(guard as guardFn, to, from, guard as RouteRecordNormalized))
        }
        return runGuardQueue(guards)
      })
      .then(() => {
        // 接下来也是相同的套路
        // 这一次队列中存放 beforeRouteUpdate 类型的守卫回调
        guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from)
        // 将路由记录中 updateGuards 里面的回调也放入进去
        for (const record of updatingRecords) {
          record.updateGuards.forEach((g) => guards.push(guardToPromise(g, to, from)))
        }
        return runGuardQueue(guards)
      })
      .then(() => {
        // 解决 beforeEnter
        guards = []
        for (const record of to.matched) {
          if (record.beforeEnter) {
            guards.push(guardToPromise(record.beforeEnter, to, from, record))
          }
        }
        return runGuardQueue(guards)
      })
      .then(() => {
        // 将 beforeRouteEnter 类型的守卫回调转为 promise 后放入队列
        // 注意是从组件上提取的
        guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from)
        // console.log('beforeRouteEnter')
        // 执行队列中的任务
        return runGuardQueue(guards)
      })
      .then(() => {
        guards = []
        // 将 beforeResolve 类型的守卫回调转为 promise 后放入队列
        for (const guard of beforeResolveGuards.list()) {
          guards.push(guardToPromise(guard as guardFn, to, from, guard as RouteRecordNormalized))
        }
        // console.log('全局的解析钩子beforeResolve')
        return runGuardQueue(guards)
      })
  }

  let ready: boolean = false

  // 该方法是用来做一个标记
  // 主要是为了设置监听器，只在第一次设置
  function markAsReady() {
    // 1. 检查路由系统是否已经准备完毕
    // 如果 ready 是 true，说明已经设置过监听器（listen）了
    if (ready) return

    // 如果没有进入到上面的 if，说明是第一次
    ready = true

    // 设置监听器
    routerHistory.listen((to) => {
      // 当路由历史发生变化时，我们首先获取目标路由和当前路由
      const targetLocation = resolve(to)
      const from = currentRoute.value

      // 之后调用 finalizeNavigation 函数完成导航
      finalizeNavigation(targetLocation, from, true)
    })
  }

  // 最终的导航方法
  function finalizeNavigation(
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    replaced?: boolean
  ) {
    // 查看是否是首次导航
    if (from === START_LOCATION_NORMALIZED || replaced) {
      routerHistory.replace(to.path)
    } else {
      routerHistory.push(to.path)
    }

    // 更新当前路由信息
    currentRoute.value = to

    // 如果是首次导航，那么我们还需要注入一个 listen 方法去更新 currentRoute 的值
    // 这样子的话数据变化后可以重新渲染视图
    markAsReady()
  }

  function pushWithRedirect(to: RouteLocationNormalized) {
    // 1. 解析出目标 location（到哪儿去）
    const targetLocation = resolve(to)

    // 2. 拿到之前的路径（从哪儿来）
    const from = currentRoute.value

    // 3. 进行导航
    navigate(targetLocation, from)
      .then(() => {
        // 完成最终的导航
        return finalizeNavigation(targetLocation, from)
      })
      .then(() => {
        // 执行 afterEach 导航守卫
        for (const guard of afterGuards.list() as ((
          to: RouteLocationNormalized,
          from: RouteLocationNormalized
        ) => void)[]) {
          guard(to, from)
        }
      })
  }

  function push(to: RouteLocationNormalized) {
    return pushWithRedirect(to)
  }

  // 创建 router 对象
  const router = {
    push,
    beforeEach: beforeGuards.add,
    afterEach: afterGuards.add,
    beforeResolve: beforeResolveGuards.add,

    // 这个 router 对象暴露出去，是要作为一个插件进行安装的
    install(app: App) {
      // 让 router 等于当前外层的 router 对象
      const router = this
      // 回头外部可以通过 this.$router 拿到 router 这个对象
      app.config.globalProperties.$router = router

      // 回头外部可以通过 this.$route 拿到当前的路由
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute)
      })

      const reactiveRoute: ReactiveRoute = {}

      // 接下来为 reactiveRoute 添加值，并且每一项值都是计算属性
      for (const key in START_LOCATION_NORMALIZED) {
        reactiveRoute[key] = computed(() => (currentRoute.value as any)[key])
      }

      // 接下来进行依赖注入
      app.provide('router', router as any)
      app.provide('route location', reactive(reactiveRoute))

      // 注册组件
      // routerlink 和 routerview
      app.component('RouterLink', RouterLink as any)
      app.component('RouterView', RouterView)

      // 如果是初始安装 router
      // 我们需要通过路由系统先进行一次跳转
      if (currentRoute.value == START_LOCATION_NORMALIZED) {
        push(routerHistory.location as any)
      }
    }
  }

  return router
}

